# Copyright 2003, 2004 Douglas Gregor
# Copyright 2003, 2004, 2005 Vladimir Prus
# Copyright 2006 Rene Rivera
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE.txt or copy at
# https://www.bfgroup.xyz/b2/LICENSE.txt)

# This module defines rules to handle generation of various outputs from source
# files documented with doxygen comments. The supported transformations are:
#
# * Source -> Doxygen XML -> BoostBook XML
# * Source -> Doxygen HTML
#
# The type of transformation is selected based on the target requested. For
# BoostBook XML, the default, specifying a target with an ".xml" suffix, or an
# empty suffix, will produce a <target>.xml and <target>.boostbook. For Doxygen
# HTML specifying a target with an ".html" suffix will produce a directory
# <target> with the Doxygen html files, and a <target>.html file redirecting to
# that directory.

import alias ;
import boostbook ;
import "class" : new ;
import common ;
import feature ;
import make ;
import modules ;
import generators ;
import os ;
import param ;
import path ;
import print ;
import project ;
import property ;
import stage ;
import targets ;
import toolset ;
import type ;
import utility ;
import xsltproc ;
import virtual-target ;


# Use to specify extra configuration parameters. These get translated into a
# doxyfile which configures the building of the docs.
feature.feature "doxygen:param" : : free ;

# Specify the "<xsl:param>boost.doxygen.header.prefix" XSLT option.
feature.feature prefix : : free ;

# Specify the "<xsl:param>boost.doxygen.reftitle" XSLT option.
feature.feature reftitle : : free ;

# Which processor to use for various translations from Doxygen.
feature.feature doxygen.processor : xsltproc doxproc : propagated implicit ;

# To generate, or not, index sections.
feature.feature doxygen.doxproc.index : no yes : propagated incidental ;

# The ID for the resulting BoostBook reference section.
feature.feature doxygen.doxproc.id : : free ;

# The title for the resulting BoostBook reference section.
feature.feature doxygen.doxproc.title : : free ;

# Location for images when generating XML
feature.feature "doxygen:xml-imagedir" : : free ;

# Indicates whether the entire directory should be deleted
feature.feature doxygen.rmdir : off on : optional incidental ;

# Doxygen configuration input file.
type.register DOXYFILE : doxyfile ;

# Doxygen XML multi-file output.
type.register DOXYGEN_XML_MULTIFILE : xml-dir : XML ;

# Doxygen XML coallesed output.
type.register DOXYGEN_XML : doxygen : XML ;

# Doxygen HTML multifile directory.
type.register DOXYGEN_HTML_MULTIFILE : html-dir : HTML ;

# Redirection HTML file to HTML multifile directory.
type.register DOXYGEN_HTML : : HTML ;

type.register DOXYGEN_XML_IMAGES : doxygen-xml-images ;


# Initialize the Doxygen module. Parameters are:
#   name: the name of the 'doxygen' executable. If not specified, the name
#         'doxygen' will be used
#
rule init ( name ? )
{
    if ! $(.initialized)
    {
        .initialized = true ;

        .doxproc = [ modules.binding $(__name__) ] ;
        .doxproc = $(.doxproc:D)/doxproc.py ;

        generators.register-composing doxygen.headers-to-doxyfile
            : H HPP CPP MARKDOWN : DOXYFILE ;
        generators.register-standard doxygen.run
            : DOXYFILE : DOXYGEN_XML_MULTIFILE ;
        generators.register-standard doxygen.xml-dir-to-boostbook
            : DOXYGEN_XML_MULTIFILE : BOOSTBOOK : <doxygen.processor>doxproc ;
        generators.register-xslt doxygen.xml-to-boostbook
            : DOXYGEN_XML : BOOSTBOOK : <doxygen.processor>xsltproc ;
        generators.register-xslt doxygen.collect
            : DOXYGEN_XML_MULTIFILE : DOXYGEN_XML ;
        generators.register-standard doxygen.run
            : DOXYFILE : DOXYGEN_HTML_MULTIFILE ;
        generators.register-standard doxygen.html-redirect
            : DOXYGEN_HTML_MULTIFILE : DOXYGEN_HTML ;
        generators.register-standard doxygen.copy-latex-pngs
            : DOXYGEN_HTML : DOXYGEN_XML_IMAGES ;

        IMPORT $(__name__) : doxygen : : doxygen ;
    }

    if $(name)
    {
        modify-config ;
        .doxygen = $(name) ;
        check-doxygen ;
    }

    if ! $(.doxygen)
    {
        check-doxygen ;
    }
}


local rule freeze-config ( )
{
    if ! $(.initialized)
    {
        import errors ;
        errors.user-error doxygen must be initialized before it can be used. ;
    }
    if ! $(.config-frozen)
    {
        .config-frozen = true ;
        if [ .is-cygwin ]
        {
            .is-cygwin = true ;
        }
    }
}


local rule modify-config ( )
{
    if $(.config-frozen)
    {
        import errors ;
        errors.user-error "Cannot change doxygen after it has been used." ;
    }
}


local rule check-doxygen ( )
{
    if --debug-configuration in [ modules.peek : ARGV ]
    {
        ECHO "notice:" using doxygen ":" $(.doxygen) ;
    }
    local extra-paths ;
    if [ os.name ] = NT
    {
        local ProgramFiles = [ modules.peek : ProgramFiles ] ;
        if $(ProgramFiles)
        {
            extra-paths = "$(ProgramFiles:J= )" ;
        }
        else
        {
            extra-paths = "C:\\Program Files" ;
        }
    }
    .doxygen = [ common.get-invocation-command doxygen : doxygen : $(.doxygen) :
        $(extra-paths) ] ;
}


rule name ( )
{
    freeze-config ;
    return $(.doxygen) ;
}


local rule .is-cygwin ( )
{
    if [ os.on-windows ]
    {
        local file = [ path.make [ modules.binding $(__name__) ] ] ;
        local dir = [ path.native [ path.join [ path.parent $(file) ] doxygen ]
            ] ;
        local command = cd \"$(dir)\" "&&" \"$(.doxygen)\"
            windows-paths-check.doxyfile 2>&1 ;
        command = $(command:J=" ") ;
        result = [ SHELL $(command) ] ;
        if [ MATCH "(Parsing file /)" : $(result) ]
        {
            return true ;
        }
    }
}


# Runs Doxygen on the given Doxygen configuration file (the source) to generate
# the Doxygen files. The output is dumped according to the settings in the
# Doxygen configuration file, not according to the target! Because of this, we
# essentially "touch" the target file, in effect making it look like we have
# really written something useful to it. Anyone that uses this action must deal
# with this behavior.
#
actions doxygen-action
{
    $(RM) "$(*.XML)" & "$(NAME:E=doxygen)" "$(>)" && echo "Stamped" > "$(<)"
}


# Runs the Python doxproc XML processor.
#
actions doxproc
{
    python "$(DOXPROC)" "--xmldir=$(>)" "--output=$(<)" "$(OPTIONS)" "--id=$(ID)" "--title=$(TITLE)"
}


rule translate-path ( path )
{
    freeze-config ;
    if [ os.on-windows ]
    {
        if [ os.name ] = CYGWIN
        {
            if $(.is-cygwin)
            {
                return $(path) ;
            }
            else
            {
                return $(path:W) ;
            }
        }
        else
        {
            if $(.is-cygwin)
            {
                match = [ MATCH "^(.):(.*)" : $(path) ] ;
                if $(match)
                {
                    return /cygdrive/$(match[1])$(match[2]:T) ;
                }
                else
                {
                    return $(path:T) ;
                }
            }
            else
            {
                return $(path) ;
            }
        }
    }
    else
    {
        return $(path) ;
    }
}

toolset.uses-features doxygen.headers-to-doxyfile : "<doxygen:param>" ;

# Generates a doxygen configuration file (doxyfile) given a set of C++ sources
# and a property list that may contain <doxygen:param> features.
#
rule headers-to-doxyfile ( target : sources * : properties * )
{
    local text = "# Generated by B2 version 2" ;

    local output-dir ;

    # Translate <doxygen:param> into command line flags.
    for local param in [ feature.get-values <doxygen:param> : $(properties) ]
    {
        local namevalue = [ MATCH "([^=]*)=(.*)" : $(param) ] ;
        if $(namevalue[1]) = OUTPUT_DIRECTORY
        {
            output-dir = [ translate-path [ utility.unquote $(namevalue[2]) ] ]
                ;
            text += "OUTPUT_DIRECTORY = \"$(output-dir)\"" ;
        }
        else
        {
            text += "$(namevalue[1]) = $(namevalue[2])" ;
        }
    }

    if ! $(output-dir)
    {
        output-dir = [ translate-path [ on $(target) return $(LOCATE) ] ] ;
        text += "OUTPUT_DIRECTORY = \"$(output-dir)\"" ;
    }

    local headers ;
    for local header in $(sources:G=)
    {
        header = [ translate-path $(header) ] ;
        headers += \"$(header)\" ;
    }

    # Doxygen generates LaTex by default. So disable it unconditionally, or at
    # least until someone needs, and hence writes support for, LaTex output.
    text += "GENERATE_LATEX = NO" ;
    text += "INPUT = $(headers:J= )" ;
    print.output $(target) plain ;
    print.text $(text) : true ;
}

toolset.uses-features doxygen.run : <doxygen.rmdir> "<doxygen:param>" ;

# Run Doxygen. See doxygen-action for a description of the strange properties of
# this rule.
#
rule run ( target : source : properties * )
{
    freeze-config ;
    if <doxygen.rmdir>on in $(properties)
    {
        local output-dir = [ path.make [ MATCH
            "<doxygen:param>OUTPUT_DIRECTORY=\"?([^\"]*)" : $(properties) ] ] ;
        local html-dir = [ path.make [ MATCH <doxygen:param>HTML_OUTPUT=(.*) :
            $(properties) ] ] ;
        if $(output-dir) && $(html-dir) &&
            [ path.glob $(output-dir) : $(html-dir) ]
        {
            HTMLDIR on $(target) = [ path.native [ path.join $(output-dir)
                $(html-dir) ] ] ;
            rm-htmldir $(target) ;
        }
    }
    doxygen-action $(target) : $(source) ;
    NAME on $(target) = $(.doxygen) ;
    RM on $(target) = [ modules.peek common : RM ] ;
    *.XML on $(target) = [ path.native [ path.join [ path.make [ on $(target)
        return $(LOCATE) ] ] $(target:B:S=) *.xml ] ] ;
}


if [ os.name ] = NT
{
    RMDIR = rmdir /s /q ;
}
else
{
    RMDIR = rm -rf ;
}

actions quietly rm-htmldir
{
    $(RMDIR) $(HTMLDIR)
}


# The rules below require BoostBook stylesheets, so we need some code to check
# that the boostbook module has actually been initialized.
#
rule check-boostbook ( )
{
    if ! [ modules.peek boostbook : .initialized ]
    {
        import errors ;
        errors.user-error
            : The boostbook module is not initialized you have attempted to use
            : the 'doxygen' toolset, which requires BoostBook, but never
            : initialized BoostBook.
            : "Hint:" add 'using boostbook \;' to your user-config.jam. ;
    }
}


# Collect the set of Doxygen XML files into a single XML source file that can be
# handled by an XSLT processor. The source is completely ignored (see
# doxygen-action), because this action picks up the Doxygen XML index file xml/
# index.xml. This is because we can not teach Doxygen to act like a NORMAL
# program and take a "-o output.xml" argument (grrrr). The target of the
# collection will be a single Doxygen XML file.
#
rule collect ( target : source : properties * )
{
    check-boostbook ;
    local collect-xsl-dir
        = [ path.native [ path.join [ boostbook.xsl-dir ] doxygen collect ] ] ;
    local source-path
        = [ path.make [ on $(source) return $(LOCATE) ] ] ;
    local collect-path
        = [ path.root [ path.join $(source-path) $(source:B) ] [ path.pwd ] ] ;
    local native-path
        = [ path.native $(collect-path) ] ;
    local real-source
        = [ path.native [ path.join $(collect-path) index.xml ] ] ;
    xsltproc.xslt $(target) : $(real-source) $(collect-xsl-dir:S=.xsl)
        : <xsl:param>doxygen.xml.path=$(native-path) ;
}

toolset.uses-features doxygen.xml-to-boostbook : <prefix> <reftitle> ;

# Translate Doxygen XML into BoostBook.
#
rule xml-to-boostbook ( target : source : properties * )
{
    check-boostbook ;
    local xsl-dir = [ boostbook.xsl-dir ] ;
    local d2b-xsl = [ path.native [ path.join [ boostbook.xsl-dir ] doxygen
        doxygen2boostbook.xsl ] ] ;

    local xslt-properties = $(properties) ;
    for local prefix in [ feature.get-values <prefix> : $(properties) ]
    {
        xslt-properties += "<xsl:param>boost.doxygen.header.prefix=$(prefix)" ;
    }
    for local title in [ feature.get-values <reftitle> : $(properties) ]
    {
        xslt-properties += "<xsl:param>boost.doxygen.reftitle=$(title)" ;
    }

    xsltproc.xslt $(target) : $(source) $(d2b-xsl) : $(xslt-properties) ;
}


toolset.flags doxygen.xml-dir-to-boostbook OPTIONS <doxygen.doxproc.index>yes :
    --enable-index ;
toolset.flags doxygen.xml-dir-to-boostbook ID <doxygen.doxproc.id> ;
toolset.flags doxygen.xml-dir-to-boostbook TITLE <doxygen.doxproc.title> ;


rule xml-dir-to-boostbook ( target : source : properties * )
{
    DOXPROC on $(target) = $(.doxproc) ;
    LOCATE on $(source:S=) = [ on $(source) return $(LOCATE) ] ;
    doxygen.doxproc $(target) : $(source:S=) ;
}


# Generate the HTML redirect to HTML dir index.html file.
#
rule html-redirect ( target : source : properties * )
{
    local uri = "$(target:B)/index.html" ;
    print.output $(target) plain ;
    print.text
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"
    \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">
<html xmlns=\"http://www.w3.org/1999/xhtml\">
<head>
  <meta http-equiv=\"refresh\" content=\"0; URL=$(uri)\" />

  <title></title>
</head>

<body>
  Automatic redirection failed, please go to <a href=
  \"$(uri)\">$(uri)</a>.
</body>
</html>
"
        : true ;
}

rule copy-latex-pngs ( target : source : requirements * )
{
    local directory = [ path.native [ feature.get-values <doxygen:xml-imagedir>
        : $(requirements) ] ] ;
    local location = [ on $(target) return $(LOCATE) ] ;

    local pdf-location = [ path.native [ path.join [ path.make $(location) ]
        [ path.make $(directory) ] ] ] ;
    local html-location = [ path.native [ path.join . html [ path.make
        $(directory) ] ] ] ;

    common.MkDir $(pdf-location) ;
    common.MkDir $(html-location) ;

    DEPENDS $(target) : $(pdf-location) $(html-location) ;

    if [ os.name ] = NT
    {
        CP on $(target) = copy /y ;
        FROM on $(target) = \\*.png ;
        TOHTML on $(target) = .\\html\\$(directory) ;
        TOPDF on $(target) = \\$(directory) ;
    }
    else
    {
        CP on $(target) = cp ;
        FROM on $(target) = /*.png ;
        TOHTML on $(target) = ./html/$(directory) ;
        TOPDF on $(target) = $(target:D)/$(directory) ;
    }
}

actions copy-latex-pngs
{
    $(CP) $(>:S=)$(FROM) $(TOHTML)
    $(CP) $(>:S=)$(FROM) $(<:D)$(TOPDF)
    echo "Stamped" > "$(<)"
}


# Building latex images for doxygen XML depends on latex, dvips, and gs being in
# your PATH. This is true for most Unix installs, but not on Win32, where you
# will need to install MkTex and Ghostscript and add these tools to your path.

actions check-latex
{
    latex -version >$(<)
}

actions check-dvips
{
    dvips -version >$(<)
}

if [ os.name ] = "NT"
{
    actions check-gs
    {
        gswin32c -version >$(<)
    }
}
else
{
    actions check-gs
    {
        gs -version >$(<)
    }
}


local rule check-tools-targets ( project )
{
    if ! $(.check-tools-targets)
    {
        # Find the root project.
        #
        # This is a best effort attempt to avoid using different locations for
        # storing *.check files depending on which project imported the doxygen
        # toolset first. The files are stored in a location related to the
        # project's root project. Note that this location may change depending
        # on the folder the build was run from in case the build uses multiple
        # related projects with their own Jamroot separate modules.
        local project-module = [ $(project).project-module ] ;
        local root-module = [ project.get-jamroot-module $(project-module) ] ;
        if ! $(root-module)
        {
            import errors ;
            if [ project.is-config-module $(project-module) ]
            {
                errors.user-error doxygen targets can not be declared in Boost
                    Build's configuration modules. ;
            }
            else
            {
                errors.user-error doxygen targets can not be declared in
                    standalone projects. : use a Jamfile/Jamroot project
                    instead. ;
            }
        }
        local root-project = [ project.target $(root-module) ] ;

        local targets =
            [ new file-target latex.check : : $(root-project) : [ new action :
                doxygen.check-latex ] ]
            [ new file-target dvips.check : : $(root-project) : [ new action :
                doxygen.check-dvips ] ]
            [ new file-target gs.check : : $(root-project) : [ new action :
                doxygen.check-gs ] ] ;

        for local target in $(targets)
        {
            .check-tools-targets += [ virtual-target.register $(target) ] ;
        }
    }
    return $(.check-tools-targets) ;
}


project.initialize $(__name__) ;
project doxygen ;

class doxygen-check-tools-target-class : basic-target
{
    rule construct ( name : sources * : property-set )
    {
        IMPORT doxygen : check-tools-targets : $(__name__) :
            doxygen.check-tools-targets ;
        return [ property-set.empty ] [ doxygen.check-tools-targets [ project ]
            ] ;
    }
}


# Declares a metatarget for collecting version information on different external
# tools used in this module.
#
rule check-tools ( target )
{
    freeze-config ;
    targets.create-metatarget doxygen-check-tools-target-class :
        [ project.current ] : $(target) ;
}


# User-level rule to generate HTML files or BoostBook XML from a set of headers
# via Doxygen.
#
rule doxygen ( target : sources + : requirements * : default-build * :
    usage-requirements * )
{
    param.handle-named-params
        sources requirements default-build usage-requirements ;
    requirements += <format>none ;
    freeze-config ;
    local project = [ project.current ] ;

    if $(target:S) = .html
    {
        # Build an HTML directory from the sources.
        local html-location = [ feature.get-values <location> : $(requirements)
            ] ;
        local output-dir ;
        if [ $(project).get build-dir ]
        {
            # Explicitly specified build dir. Add html at the end.
            output-dir = [ path.join [ $(project).build-dir ]
                $(html-location:E=html) ] ;
        }
        else
        {
            # Trim 'bin' from implicit build dir, for no other reason than
            # backward compatibility.
            output-dir = [ path.join [ path.parent [ $(project).build-dir ] ]
                $(html-location:E=html) ] ;
        }
        output-dir = [ path.root $(output-dir) [ path.pwd ] ] ;
        local output-dir-native = [ path.native $(output-dir) ] ;
        requirements = [ property.change $(requirements) : <location> ] ;

        # The doxygen configuration file.
        targets.create-typed-target DOXYFILE : $(project) : $(target:S=.tag)
            : $(sources)
            : $(requirements)
                <doxygen:param>GENERATE_HTML=YES
                <doxygen:param>GENERATE_XML=NO
                <doxygen:param>"OUTPUT_DIRECTORY=\"$(output-dir-native)\""
                <doxygen:param>HTML_OUTPUT=$(target:B)
            : $(default-build) ;
        $(project).mark-target-as-explicit $(target:S=.tag) ;

        # The html directory to generate by running doxygen.
        targets.create-typed-target DOXYGEN_HTML_MULTIFILE : $(project)
            : $(target:S=.dir)  # Name.
            : $(target:S=.tag)  # Sources.
            : $(requirements)
                <doxygen:param>"OUTPUT_DIRECTORY=\"$(output-dir-native)\""
                <doxygen:param>HTML_OUTPUT=$(target:B)
            : $(default-build) ;
        $(project).mark-target-as-explicit $(target:S=.dir) ;

        # The redirect html file into the generated html.
        targets.create-typed-target DOXYGEN_HTML : $(project) : $(target)
            : $(target:S=.dir)  # Sources.
            : $(requirements) <location>$(output-dir)
            : $(default-build) ;
    }
    else
    {
        # Build a BoostBook XML file from the sources.
        local location-xml = [ feature.get-values <location> : $(requirements) ]
            ;
        requirements = [ property.change $(requirements) : <location> ] ;
        local target-xml = $(target:B=$(target:B)-xml) ;

        # Check whether we need to build images.
        local images-location = [ feature.get-values <doxygen:xml-imagedir> :
            $(requirements) ] ;
        if $(images-location)
        {
            # Prepare a metatarget for collecting used external tool version
            # information. We use only one such metatarget as they always
            # produce the same files and we do not want to deal with multiple
            # metatargets having matching names, causing 'ambiguous variants'
            # errors.
            if ! $(.check-tools)
            {
                # FIXME: Since we have the check-tools target object reference,
                # see how we can use that instead of having to construct a valid
                # target reference string for use in <dependency> property
                # values.
                local project-id = --doxygen.check-tools-project-- ;
                local target-id = --doxygen.check-tools-- ;
                local pm = [ $(project).project-module ] ;
                project.register-id $(project-id) : $(pm) ;
                check-tools $(target-id) ;
                .check-tools = /$(project-id)//$(target-id) ;
            }

            doxygen $(target).doxygen-xml-images.html : $(sources) :
                $(requirements)
                <doxygen.rmdir>on
                <doxygen:param>QUIET=YES
                <doxygen:param>WARNINGS=NO
                <doxygen:param>WARN_IF_UNDOCUMENTED=NO
                <dependency>$(.check-tools) ;
            $(project).mark-target-as-explicit $(target).doxygen-xml-images.html
                ;

            targets.create-typed-target DOXYGEN_XML_IMAGES : $(project)
                : $(target).doxygen-xml-images  # Name.
                : $(target).doxygen-xml-images.html  # Sources.
                : $(requirements)
                : $(default-build) ;
            $(project).mark-target-as-explicit $(target).doxygen-xml-images ;

            if ! [ MATCH (/)$ : $(images-location) ]
            {
                images-location = $(images-location)/ ;
            }

            requirements +=
                <dependency>$(target).doxygen-xml-images
                <xsl:param>boost.doxygen.formuladir=$(images-location) ;
        }

        # The doxygen configuration file.
        targets.create-typed-target DOXYFILE : $(project) : $(target-xml:S=.tag)
            : $(sources)
            : $(requirements)
                <doxygen:param>GENERATE_HTML=NO
                <doxygen:param>GENERATE_XML=YES
                <doxygen:param>XML_OUTPUT=$(target-xml)
            : $(default-build) ;
        $(project).mark-target-as-explicit $(target-xml:S=.tag) ;

        # The Doxygen XML directory for the processed source files.
        targets.create-typed-target DOXYGEN_XML_MULTIFILE : $(project)
            : $(target-xml:S=.dir)  # Name.
            : $(target-xml:S=.tag)  # Sources.
            : $(requirements)
            : $(default-build) ;
        $(project).mark-target-as-explicit $(target-xml:S=.dir) ;

        # The resulting BoostBook file is generated by the processor tool. The
        # tool can be either the xsltproc plus accompanying XSL scripts. Or it
        # can be the python doxproc.py script.
        targets.create-typed-target BOOSTBOOK : $(project) : $(target-xml)
            : $(target-xml:S=.dir)  # Sources.
            : $(requirements)
            : $(default-build) ;
        $(project).mark-target-as-explicit $(target-xml) ;

        stage $(target:S=.xml)  # Name.
            : $(target-xml)  # Sources.
            : $(requirements)
                <location>$(location-xml:E=.)
                <name>$(target:S=.xml)
            : $(default-build) ;
        $(project).mark-target-as-explicit $(target:S=.xml) ;

        # TODO: See why this alias target is used here instead of simply naming
        # the previous stage target $(target) and having it specify the alias
        # target's usage requirements directly.
        alias $(target) : : $(requirements) : $(default-build) :
            $(usage-requirements) <dependency>$(target:S=.xml) ;
    }
}
